package com.tnsw.facebookautoliker.helper;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.concurrent.CopyOnWriteArrayList;

import android.content.Context;
import android.util.Log;

import com.tnsw.coreutils.AsyncManager;
import com.tnsw.coreutils.MyLog;
import com.tnsw.coreutils.database.DBActionListener;
import com.tnsw.coreutils.database.RecordBase;
import com.tnsw.facebookautoliker.services.AutoLikerService;

public class FacebookBGManager {

	private static boolean sIsRunning = false;
	private static FacebookBGManager sInstance;
	
	public static int MAX_CONCURRENT_ACTION = 1;
	public static int MIN_FREQUENCE_MIL = 5000;		// Not allow same signature come being executed in 5000 millisec
	
	private String tag = "FacebookBGManager";
	private int concurrent = 0;
	private Context context;
	
	private Hashtable<String, FacebookCommandObject> hashLogs;
	private Hashtable<String, CommandCounter> hashCommandCount;		// <RequestId, CommandCounter>
	private CopyOnWriteArrayList<FacebookActionListener> listeners;
	private Hashtable<String, Long> hashLastRun;
	
	private FacebookCommandObject currentFbco;
	private boolean _isPaused = false;
	
	public FacebookBGManager(Context context) {
		Log.d(tag, "Constructor(context)");
		
		this.context = context;
		this.hashCommandCount = new Hashtable<String, CommandCounter>();
		this.hashLogs = new Hashtable<String, FacebookCommandObject>();
		this.hashLastRun = new Hashtable<String, Long>();
		
		this.listeners = new CopyOnWriteArrayList<FacebookActionListener>();
		
		sInstance = this;
		sIsRunning = true;
	}
	
	public static FacebookBGManager getInstance(Context context) {
		if (sIsRunning == false) {
			sInstance = new FacebookBGManager(context);
		}
		
		return(sInstance);
	}
	
	public void pause() {
		this._isPaused = true;
	}
	
	public void resume() {
		this._isPaused = false;
	}
	
	public boolean isPaused() {
		return(this._isPaused);
	}
	
	public int getConcurrent() {
		return(this.concurrent);
	}
	
	/**
	 * do check AutoLikerTime status and recreate/restart if it's stopped longer than it should be for 2 period
	 */
	public void checkTimerStatus() {
		
		AutoLikerService aService = AutoLikerService.getInstance();
		if (aService != null) {
			if (aService.getTimer() != null) {
				long diff = (long) Math.abs(aService.getTimerTask().getLastExecute()-System.currentTimeMillis())*2;
				if (diff > aService.getTimerIntervalMinute()*60*1000) {
					// Too long from last execute, looks like Timer is death (but not null).
					MyLog.w(tag, "Too long since last executed, trying to restart timer");
					aService.startTimer();
				} else {
					MyLog.d(tag, "Timer status is healthy.");
				}
				
			} else {
				MyLog.w(tag, "checkTimerStatus - Timer is null for some reason, trying to restart Timer inside AutoLikerService, trying to restart timer");
				aService.startTimer();
			}
		} else {
			MyLog.e(tag, "checkTimerStatus [pending] - Found AutoLikerService is disappeared");
		}
	}
	
	public void execute(FacebookCommandObject fbco) {
		if (this._isPaused == true) {
			this.concurrent = 0;
			MyLog.w(tag, "FacebookBGManager is paused. Concurrent is set to zero and not executing any further command");
			return;
		}
		
		boolean b = fbco.prepare();
		if (b == false) {
			MyLog.w(tag, fbco.toString() + " is terminated because prepare==false");
			return;
		}
		
		if (fbco.getChildrenCommandCount() > 0) {
			CommandCounter cc = new CommandCounter(fbco.requestId(), fbco.getChildrenCommandCount());
			this.hashCommandCount.put(fbco.requestId(), cc);
			MyLog.d(tag, "added new command(with children), id=" + fbco.requestId() + ", class=" + fbco.getClass().getName() + ", children=" + fbco.getChildrenCommandCount());
		}
		
		long ll = 0;
		
		if (this.concurrent >= MAX_CONCURRENT_ACTION) {
			while (this.concurrent >= MAX_CONCURRENT_ACTION) {
				try {
					int waitMil = 100;
					//MyLog.d(tag, "Wait for " + waitMil + " millisec");
					if (this.currentFbco != null) {
						//MyLog.w(tag, "Throttle limited, Object:" + fbco.toString() + " is waiting for " + this.currentFbco.toString() + ", to be finished first");
						long diff = System.currentTimeMillis() - this.currentFbco.getExecuteTime();
						if (diff > this.currentFbco.getMaxWaitMil()) {
							ll = System.currentTimeMillis();
							MyLog.w(tag, "Timeout for object:" + this.currentFbco.toString() + ", debug=" + ll);
							this.updateCommandStatus(this.currentFbco, 700, null);
							break;
						}
					}
				
					Thread.currentThread().sleep(waitMil);
				} catch (Exception e) {
					MyLog.e(tag, "execute() - sleep " + e.toString(), e);
				}
				
			}	
		}
		
		if (ll != 0l) {
			MyLog.w(tag, "debug=" + ll);
		}
		
		if (fbco.requiredSignature() == true) {
			if (this.hashLastRun.containsKey(fbco.getSignature()) == false) {
				this.hashLastRun.put(fbco.getSignature(), System.currentTimeMillis());
				
			} else {
				long l = this.hashLastRun.get(fbco.getSignature());
				long nowl = System.currentTimeMillis();
				long diff = Math.abs(nowl-l);
				
				int min = MIN_FREQUENCE_MIL;
				if (fbco.getMinFrequenceMillisecond() != -1) {
					min = fbco.getMinFrequenceMillisecond();
				}
								
				if (diff < MIN_FREQUENCE_MIL) {
					MyLog.w(tag, "Too often to execute the command(" + fbco.getSignature() + "), CANCELLED RUN");
					
					return;
				} else {
					MyLog.d(tag, "Frequence is OK to execute:" + fbco.getSignature() + ", diff=" + diff);
					this.hashLastRun.put(fbco.getSignature(), nowl);
				}
			}
		}
				
		this.currentFbco = fbco;
		MyLog.d(tag, "Executing: fbco:" + this.currentFbco.toString());
		fbco.execute();
	}
	
	public void addCommand(FacebookCommandObject fbco) {
		this.hashLogs.put(fbco.requestId(), fbco);
	}
	
	public String getLogs() {
		StringBuffer sb = new StringBuffer();
		
		try {
			Object[] objs = this.hashLogs.values().toArray();
			int len = objs.length;
			int i;
			for (i = 0; i < len; i++) {
				FacebookCommandObject fbco = (FacebookCommandObject) objs[i];
				sb.append(fbco.toString() + "\r\n");
			}
			
		} catch (Exception e) {
			MyLog.e(tag, "getLogs - " + e.toString(), e);
		}
		
		return(sb.toString());
	}
	
	public boolean isRequestIdValid(String requestId) {
		
		if (this.hashLogs.containsKey(requestId) == true) {
			return(true);
		} else {
			return(false);
		}
	}
	
	public void updateCommandStatus(FacebookCommandObject fbco, int statusCode, Exception exception) {
		if (this.hashLogs.containsKey(fbco.requestId()) == false) {
			MyLog.e(tag, "updateCommandStatus will not raise event (not found requestId=" + fbco.requestId() + ")");
			return;
		}
		
		if (fbco.applyCounter() == true) {
			this.concurrent -= 1;
			MyLog.d(tag, "counter (cc=" + this.concurrent + ") decreased by " + fbco.getClass().getName());
		}
		
		if (fbco.getChildrenCommandCount() > 0) {
			MyLog.e(tag, "ignore updateCommandStatus(" + fbco.getClass().getName()+ ", id=" + fbco.requestId() + ") because it's parent and having children(" + fbco.getChildrenCommandCount() + "). This will be broadcast when all children are done");
			return;
		}
		
		this.hashLogs.remove(fbco.requestId());
		this.updateCounter(fbco, statusCode, exception);
	}
	
	public void increaseCounter(FacebookCommandObject fbco) {
		if ( (fbco != null) && (fbco.applyCounter() == true) ) {
			this.concurrent += 1;
			MyLog.d(tag, "counter(cc=" + this.concurrent + ") increased by " + fbco.getClass().getName());
		}
		
	}
	
	private void updateCounter(FacebookCommandObject fbco, int statusCode, Exception tr) {
		
		if (fbco.getParent() != null) {
			// this is child
			
			this.broadcastAction(fbco, statusCode, tr);
			
			CommandCounter cc = this.hashCommandCount.get(fbco.getParent().requestId());
			if (cc != null) {
				cc.increateCurrentCount();
				MyLog.d(tag, "parent_current_count=" + cc.getCurrentCount() + "/" + cc.getMaxChildrenCount());
				if (cc.getMaxChildrenCount() == cc.getCurrentCount()) {
					this.updateCounter(fbco.getParent(), 200, null);
				}	
			} else {
				MyLog.w(tag, "Cannot get parent CommandCount from hashCommandCount, parent_requestId=" + fbco.getParent().requestId());
			}
			
			
			
		} else { // this is parent or single command, just broadcast
			Log.d(tag, "broadcast(" + fbco.getClass().getName() + ", id=" +fbco.requestId() + ", statusCode=" + statusCode + ")");
			this.broadcastAction(fbco, statusCode, tr);
			this.hashCommandCount.remove(fbco.requestId());
		}
	}
	
	public void registerActionListener(FacebookActionListener listener) {
		if (this.listeners.contains(listener) == false) {
			this.listeners.add(listener);
			MyLog.d(tag, "registeredAction: " + listener.getClass().getName() + ", hashCode=" + listener.hashCode());
		} else {
			MyLog.w(tag, "duplicatedRegister: " + listener.getClass().getName() + ", hastCode=" + listener.hashCode());
		}
	}
	
	public void unregisterActionListener(FacebookActionListener listener) {
		if (this.listeners.contains(listener)) {
			this.listeners.remove(listener);
			MyLog.d(tag, "unregisteredAction: " + listener.getClass().getName());	
		}
		
	}
	
	public CopyOnWriteArrayList<FacebookActionListener> getListeners() {
		return(this.listeners);
	}
	
	private void broadcastAction(FacebookCommandObject fbco, int statusCode, Exception tr) {
		try {
			int size = this.listeners.size();
			int i;
			for (i = 0; i < size; i++) {
				FacebookActionListener listener = this.listeners.get(i);
				if (listener != null) {
					if (fbco == null) {
						MyLog.e(tag, "why fbco is null?");
					}
					
					if (fbco.requestId() == null) {
						MyLog.e(tag, "why fbco.requestId() is null");
					}
					
					listener.onRequestCompleted(this, fbco, statusCode, tr);
				}
			}
			
		} catch (Exception e) {
			MyLog.e(tag, "broadcastAction - " + e.toString(), e);
		}
	}
		
	public boolean cancel(String requestId) {
		boolean b = false;
		
		if (this.hashCommandCount.contains(requestId)) {
			this.hashCommandCount.remove(requestId);
		}
		
		if (this.hashLogs.containsKey(requestId)) {
			b = true;
			FacebookCommandObject fbco = this.hashLogs.get(requestId);

			this.updateCommandStatus(fbco, 1405, null);
		} else {
			MyLog.e(tag, "Cannot cancel requestId=" + requestId + ", not found in hashLogs");
		}
		
		return(b);
	}
	
}
